package org.proxywars.engine.scene.shape;

import java.util.ArrayList;

import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.state.ClipState;
import com.jme.system.DisplaySystem;

/**
 * This class extends Rectangle3d to create an easily filled rectangle.
 * It can be filled with any color or texture.
 * <p>
 * While you cannot set color gradients with functions at this time, 
 * you can easily paint the rectangle with a gradient texture.
 * 
 * @author Steven DeVries
 */
public class FilledRectangle3d extends TexturedRectangle3d {
    
    /* Constant Variables */
    private static final long serialVersionUID = 1L;
    
    /** Alignment constant: used to determine how to fill the rectangle - left to right. */
    public static final int ALIGN_LEFT = 0;
    /** Alignment constant: used to determine how to fill the rectangle - right to left. */
    public static final int ALIGN_RIGHT = 1;
    /** Alignment constant: used to determine how to fill the rectangle - center out. */
    public static final int ALIGN_TOP =2;
    /** Alignment constant: used to determine how to fill the rectangle - bottom up.*/
    public static final int ALIGN_BOTTOM = 3;
    /** Alignment constant: used to determine how to fill the rectangle - top down.*/
    public static final int ALIGN_CENTER = 4;
    
    /** Fill mode constant: used to determine how bar is filled - by clipping*/
    public static final int FILL_CLIPPED = 0;
    /** Fill mode constant: used to determine how bar is filled - by scaling*/
    public static final int FILL_SCALED = 1;
    
    
    /* Protected Variables */
    /** ClipState used to 'fill' the rectangle if the fill mode is FILL_CLIPPED */
    protected ClipState clipState;
    
    /** ArrayList of colors used to fill the rectangle. */
    // might move this to private once gradients are supported
    protected ArrayList<ColorRGBA> colors;   // Color array used to paint rectangle.
    
    
    /* Private Variables */    
    /** Determines if fill effects the Y-axis or not. */
    private boolean isVertical = true;
    /** Determines if fill effects the X-axis or not. */
    private boolean isHorizontal = true;

    private int alignHorizontal = ALIGN_LEFT;   // Horizontal Alignment of filling
    private int alignVertical = ALIGN_TOP;        // Vertical Alignment of filling
    
    private boolean isClipped = true;                // Determines is Clipping or Scaling is used.
    
    private float xFillScale = 1f;    // scales used when clipping or scaling
    private float yFillScale = 1f;    // % of the width/height to display

    /* Constructors */
    /**
     * Convenience constructor for child classes.
     * Should only ever be used on components that load a skin.
     */
    public FilledRectangle3d() {
    }

    /**
     * Creates a white, 200x100 rectangle at (0,0).
     * 
     * @param name     Name of the rectangle.
     */
    public FilledRectangle3d(String name) {
        super(name);
    }

    /**
     * Creates a white rectangle of specified size at (0,0).
     * 
     * @param name     Name of the rectangle.
     * @param width     width of the rectangle.
     * @param height    height of the rectangle.
     */
    public FilledRectangle3d(String name, int width, int height) {
        super(name, width, height);
    }

    /**
     * Creates a white rectangle with the specified location and size.
     * 
     * @param name     Name of the rectangle.
     * @param x           x location of top left corner of the rectangle.
     * @param y            y location of the top left corner of the rectangle.
     * @param width     width of the rectangle.
     * @param height    height of the rectangle.
     */
    public FilledRectangle3d(String name, int x, int y, int width, int height) {
        super(name, x, y, width, height);
    }
    
    /**
     * Convenience function so that all the constructors just call one function.
     * Makes it easier to change/update or for children to initialize all the values as well.
     */
    @Override
    protected void initialize(String name, int x, int y, int width, int height) {
        /* Setup Colors ArrayList */
        colors = new ArrayList<ColorRGBA>(1);
        colors.add(ColorRGBA.white);
        
        /* Setup ClipState */
        clipState = DisplaySystem.getDisplaySystem().getRenderer().createClipState();
        updateClipStateEquations();
        for (int i = 0; i < 4; i++) {
            clipState.setEnableClipPlane(i, true);
        }
        setRenderState(clipState);
        
        /* initialize */
        super.initialize(name, x, y, width, height);
    }
    
    
    /* Get Methods */
    /**
     * Returns the horizontal alignment constant for this rectangle.
     * @return either ALIGN_CENTER, ALIGN_LEFT, or ALIGN_RIGHT.
     */
    public int getAlignX() {
        return alignHorizontal;
    }
    
    /**
     * Returns the vertical alignment constant for this rectangle.
     * @return either ALIGN_CENTER, ALIGN_BOTTOM, or ALIGN_TOP.
     */
    public int getAlignY() {
        return alignVertical;
    }
    
    /**
     * Returns if the rectangle is filling horizontally or staying full.
     * @return  is the rectangle scaling.
     */
    public boolean getFillDirectionX() {
        return isHorizontal;
    }
    
    /**
     * Returns if the rectangle is filling vertically or staying full.
     * @return  is the rectangle scaling.
     */
    public boolean getFillDirectionY() {
        return isVertical;
    }
    
    /**
     * Returns the fill mode constant for this rectangle.
     * @return  either FILL_CLIPPED or FILL_SCALED.
     */
    public int getFillMode() {
        return (isClipped ? FILL_CLIPPED : FILL_SCALED);
    }

    /**
     * Returns the fill height of the rectangle.
     * @return  fill height of the rectangle.
     */
    public int getFillHeight() {
        return (isVertical ? (int) (yFillScale * getHeight() ): (int) getHeight() );
    }
    
    /**
     * Returns the fill width of the rectangle.
     * @return  fill width of the rectangle.
     */
    public int getFillWidth() {
        return (isHorizontal ? (int) (xFillScale * getWidth() ): (int) getWidth() );
    }
    
    /**
     * Returns the percentage that the rectangle is filled horizontally.
     * @return  x scale percent (between 0 and 1).
     */
    public float getFillX() {
        return (isHorizontal ? xFillScale : 1f );
    }
    
    /**
     * Returns the percentage that the rectangle is filled vertically.
     * @return  y scale percent (between 0 and 1).
     */
    public float getFillY() {
        return (isVertical ? yFillScale : 1f );
    }
    
    
    /* Set Methods */
    /**
     * Sets the alignment of the rectangle.
     * The rectangle is filled from the aligned side 
     * (e.g. left alignment starts on the left and fills to the right.) 
     * 
     * @param alignHorizontal    Horizontal alignment constant: ALIGN_CENTER, ALIGN_LEFT, or ALIGN_RIGHT.
     * @param alignVertical        Vertical alignment constant: ALIGN_CENTER, ALIGN_BOTTOM, or ALIGN_TOP.
     */
    public void setAlign(int alignHorizontal, int alignVertical) {
        this.alignHorizontal = alignHorizontal;
        this.alignVertical = alignVertical;
        updateFill();
    }
    
    /**
     * Sets the direction(s) this rectangle will fill in.  
     * By default, both are true.
     * This method is merely a way to easily create something like a progress bar, 
     * where you only want to allow filling in one direction.
     * 
     * @param isHorizontal      Enables horizontal filling - false = full width rectangle.
     * @param isVertical          Enables vertical filling - false = full height rectangle.
     */
    public void setFillDirection(boolean isHorizontal, boolean isVertical) {
        this.isHorizontal = isHorizontal;
        this.isVertical = isVertical;
        updateFill();
    }    
    
    /**
     * Sets the fill mode for this rectangle to either clipping or scaling.
     * For tiled or gradient textures, use clipping.
     * For textures with rounded corners, use scaling.
     * For colors, either method is fine - clipping probably takes less resources though
     * as you are not rescaling and repositioning.
     * 
     * @param fillMode  Fill constant: FILL_CLIPPED or FILL_SCALED.
     */    
    public void setFillMode(int fillMode) {        
        switch (fillMode) {
        case FILL_CLIPPED:
            isClipped = true;
            clipState.setEnabled(true);
            setLocalScale(new Vector3f(1f, 1f, 0) );    // reset scale
            break;
        case FILL_SCALED:
            isClipped = false;
            clipState.setEnabled(false);
            break;
        default:
            System.out.println("Invalid fill mode - clipping enabled.");
            isClipped = true;
            clipState.setEnabled(true);
            setLocalScale(new Vector3f(1f, 1f, 0) );    // reset scale
            break;
        }
        
        updateFill();
    }
    
    /**
     * Sets how much of the rectangle is filled with the current texture or color.
     * The starting point is determined by the Rectangles horizontal and vertical alignments.
     * 
     * @param fillWidth     width of area filled.
     * @param fillHeight    height of area filled.
     */
    public void setFillSize(int fillWidth, int fillHeight) {
        xFillScale = (float) fillWidth / width;
        yFillScale = (float) fillHeight / height;
        setFillSize(xFillScale, yFillScale);
    }    
    
    /**
     * Sets how much of the rectangle is filled with the current texture or color.
     * The starting point is determined by the Rectangles horizontal and vertical alignments.
     * 
     * @param xFillScale    percentage (0-1f) of rectangle width to fill.
     * @param yFillScale    percentage (0-1f) of rectangle height to fill.
     */
    public void setFillSize(float xFillScale, float yFillScale) {
        this.xFillScale = xFillScale;
        this.yFillScale = yFillScale;
        
        updateFill();
    }
    
    /**
     * Sets the location of the rectangle on the screen.
     * This function uses traditional (x, y) coordinates, (0,0) being top left corner.
     * The top, left corner of the rectangle will be at the specified point.
     * 
     * @param x     horizontal starting point.
     * @param y     vertical starting point.
     */
    @Override
    public void setLocation(int x, int y) {
        super.setLocation(x, y);
        updateFill();
    }    
    
    /**
     * Sets the size of the rectangle in pixels on screen.
     * 
     * @param width     width of the rectangle.
     * @param height    height of the rectangle.
     */
    @Override
    public void setSize(int width, int height) {   
        super.setSize(width, height);
        updateFill();
    }
    
    
    /* Update Methods */
    /**
     * Updates the color / texture to fill only a portion of the rectangle.
     * Kept protected so children can easily extend its functionality.
     */
    protected void updateFill()
    {
        if( isClipped ) {
            updateClipStateEquations();
        }
        else {
            updateScaledLocation();
        }
        updateRenderState();
    }
    
    /* Rescales the quad and realigns it to origin. 
     * Separated the function for easier reading/modifying of code.
     */
    private void updateScaledLocation()
    {
        /* Scale the Quad and its texture */
        float xScale = (isHorizontal ? xFillScale : 1);
        float yScale = (isVertical ? yFillScale : 1);
        
        setLocalScale(new Vector3f(xScale, yScale, 0)   );    
        
        /* Re-align the Quad */
        float screenHeight = (float) DisplaySystem.getDisplaySystem().getHeight();
        float scaledWidth = width * xScale;
        float scaledHeight = height * yScale;
        
        // values for center
        float u = origin.x + width/2;
        float v = screenHeight - (origin.y + height/2);
        
        if (alignHorizontal == ALIGN_LEFT) {
            u = origin.x + scaledWidth/2;
        }
        else if (alignHorizontal == ALIGN_RIGHT) {
            u = origin.x + width;
        }
        
        if (alignVertical == ALIGN_BOTTOM) {
            v = screenHeight - (origin.y + scaledHeight);
        }
        else if (alignVertical == ALIGN_TOP) {
            v = screenHeight - (origin.y + scaledHeight/2);
        }
        
        /* Update the location */
        setLocalTranslation(new Vector3f(u, v, 0) );        
    }
    
    /*
     * Updates Clipping Equations.
     * As above, separated for ease of reading/updating code. 
     */
    private void updateClipStateEquations() {        
        float x0, x1, y0, y1;
        float clipWidth = width * (isHorizontal ? xFillScale : 1);
        float clipHeight = height * (isVertical ? yFillScale : 1);
        
        switch(alignHorizontal) {
            case ALIGN_LEFT:
                x0 = (getCenter().x - width/2) - clipWidth;
                x1 = (getCenter().x - width/2) + clipWidth;
                break;
            case ALIGN_RIGHT:
                x0 = (getCenter().x + width/2) - clipWidth;
                x1 = (getCenter().x + width/2) + clipWidth;
                break;
            case ALIGN_CENTER:
                x0 = getCenter().x - clipWidth/2;
                x1 = getCenter().x + clipWidth/2;                
                break;
            default:    // left
                x0 = (getCenter().x - width/2) - clipWidth;
                x1 = (getCenter().x - width/2) + clipWidth;
                break;
        }
        
        switch(alignVertical) {
            case ALIGN_BOTTOM:
                y0 = (getCenter().y - height/2) - clipHeight;
                y1 = (getCenter().y - height/2) + clipHeight;
                break;
            case ALIGN_TOP:
                y0 = (getCenter().y + height/2) - clipHeight;
                y1 = (getCenter().y + height/2) + clipHeight;
                break;
            case ALIGN_CENTER:
                y0 = getCenter().y - clipHeight/2;
                y1 = getCenter().y + clipHeight/2; 
                break;
            default:    // center
                y0 = getCenter().y - clipHeight/2;
                y1 = getCenter().y + clipHeight/2; 
                break;
        }
        
        clipState.setClipPlaneEquation(0,  1f,  0f, 0, -x0);
        clipState.setClipPlaneEquation(1, -1f,  0f, 0,  x1);
        clipState.setClipPlaneEquation(2,  0f,  1f, 0, -y0);
        clipState.setClipPlaneEquation(3,  0f, -1f, 0,  y1);
    }
}
